
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.apache.commons.io.FileUtils;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.xerial.snappy.Snappy;

/**
 * @Title: Circles.java
 * @Package com.xu.opencv.image
 * @Description: OpenCV 原型檢查,文字颜色不能和印章文字颜色一样，否则无法检测
 * @author: hyacinth
 * @date: 2022年2月22日 下午22:10:14
 * @version: V-1.0.0
 * @Copyright: 2022 hyacinth
 */
public class Circles
{

	static double distance = 200d;

	static
	{
		String os = System.getProperty("os.name");
		String type = System.getProperty("sun.arch.data.model");
		if (os.toUpperCase().contains("WINDOWS"))
		{
			File lib;
			if (type.endsWith("64"))
			{
				lib = new File("D:\\BaiduNetdiskDownload\\opencv\\build\\java\\x64\\" + System.mapLibraryName("opencv_java460"));
			}
			else
			{
				lib = new File("D:\\BaiduNetdiskDownload\\opencv\\build\\java\\x86\\" + System.mapLibraryName("opencv_java460"));
			}
			System.load(lib.getAbsolutePath());
		}
	}

	// 公章检测，含圆形和椭圆形公章，不能检测方形私章
	public static void main(String[] args) throws IOException
	{
//		String sourceImagePath = "D:/a.png";
//		String targetImagePath = "D:/b.png";
		// 1.先进行印章提取
//		int[] wh = signatureProcess(sourceImagePath, targetImagePath);
		// 2.对提取的印章进行原型检测
		File dir = new File("D:/pictest");
		File targetImagePath = new File(dir, "a.jpg");
		for (File file : dir.listFiles())
		{
			if (!file.getName().equals(targetImagePath.getName()))
			{
				rgbRed(file.getAbsolutePath(), targetImagePath.getAbsolutePath());
				System.err.println(file.getName() + "是否含有：" + houghCircles(targetImagePath.getAbsolutePath()));
			}
		}
		Snappy.compress(new byte[0]);
	}

	public static boolean houghCircles(String targetImagePath) throws IOException
	{
		File outFile = new File(targetImagePath);
		if (!outFile.exists())
		{
			return false;
		}
		Mat src = Imgcodecs.imread(targetImagePath);
		Mat dst = src.clone();
		if (dst.width() <= 0)
		{
			System.err.println("文件读取失败");
			return false;
		}

//		 IMREAD_UNCHANGED = -1 ：不进行转化，比如保存为了16位的图片，读取出来仍然为16位。
//		 IMREAD_GRAYSCALE = 0 ：进行转化为灰度图，比如保存为了16位的图片，读取出来为8位，类型为CV_8UC1。
//		 IMREAD_COLOR = 1 ：进行转化为三通道图像。
//		 IMREAD_ANYDEPTH = 2 ：如果图像深度为16位则读出为16位，32位则读出为32位，其余的转化为8位。
//		 IMREAD_ANYCOLOR = 4 ：图像以任何可能的颜色格式读取
//		 IMREAD_LOAD_GDAL = 8 ：使用GDAL驱动读取文件，GDAL(Geospatial Data Abstraction
//		 Library)是一个在X/MIT许可协议下的开源栅格空间数据转换库。它利用抽象数据模型来表达所支持的各种文件格式。
//		  它还有一系列命令行工具来进行数据转换和处理。

		// 锐化 https://blog.csdn.net/ren365880/article/details/103933835
		Mat kernel = new Mat(3, 3, Imgcodecs.IMREAD_COLOR);
		kernel.put(0, 0, 0, -1, 0, -1, 5, -1, 0, -1, 0);
		Imgproc.filter2D(dst, dst, -1, kernel);
		// 高斯模糊
		Imgproc.GaussianBlur(dst, dst, new Size(3, 3), 3);
		// 中值滤波
		Imgproc.medianBlur(dst, dst, 7);

		Mat gary = new Mat();
		Imgproc.cvtColor(dst, gary, Imgproc.COLOR_BGR2GRAY);

		Mat circles = new Mat();
//		
//		 该函数使用霍夫变换来找到灰度图像中的圆。
//		 注意：通常该功能可以很好地检测圆心。但是，它可能找不到正确的半径。如果知道半径范围（minRadius和maxRadius），则可以辅助该功能。或者，
//		 您可以将maxRadius设置为负数以仅返回中心而不进行半径搜索，并使用其他过程查找正确的半径。
//		 @param图像8位单通道灰度输入图像。
//		 @param circle输出找到的圆的向量。每个向量都被编码为3或4个浮点向量（（x，y，radius））或（（x，y，radius，votes））。
//		 @param方法检测方法，请参见#HoughModes。当前，唯一实现的方法是#HOUGH_GRADIENT
//		 @param dp累加器分辨率与图像分辨率的反比。例如，如果dp = 1，则累加器具有与输入图像相同的分辨率。如果dp = 2，则累加器的宽度和高度是其一半。
//		 @param minDist检测到的圆心之间的最小距离。如果参数太小，则除了真实的圆圈外，还可能会错误地检测到多个邻居圆圈。如果太大，可能会错过一些圆。
//		 @param param1第一个方法特定的参数。在#HOUGH_GRADIENT的情况下，它是传递给Canny边缘检测器的两个阈值中的较高阈值（较低的那个要小两倍）
//		 @param param2第二个方法特定的参数。在#HOUGH_GRADIENT的情况下，它是检测阶段圆心的累加器阈值。它越小，可能会检测到更多的假圆圈。
//		 与较大的累加器值相对应的圆将首先返回。
//		 @param minRadius最小圆半径。
//		 @param maxRadius最大圆半径。如果<= 0，则使用最大图像尺寸。如果<0，则返回中心而未找到半径。
//		 
		double r = Math.pow(gary.height(), 2) + Math.pow(gary.width(), 2);
		int rr = (int) (Math.sqrt(r) / 2);
		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1, rr, 130, 38, rr > 50 ? 50 : 5, rr);
//		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1, 50, 400, 20, 5, 1000);
//		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1, 300, 400, 100, 2, 1000);
		BufferedImage sourceBufferIamge = ImageIO.read(new File(targetImagePath));
		for (int i = 0; i < circles.rows(); i++)
		{
			for (int j = 0, num = circles.cols(); j < num; j++)
			{
				double[] vCircle = circles.get(i, j);

				Point center = new Point(vCircle[0], vCircle[1]);
				int radius = (int) Math.round(vCircle[2]);

				// circle center
				Imgproc.circle(src, center, 1, new Scalar(0, 255, 0), 2);
				// circle outline
				Imgproc.circle(src, center, radius, new Scalar(0, 0, 255), 2);
				// 利用圆心和半径，取360个点，画一个园，记录圆上的点
				int connectPoint = 0;
				for (int n = 0; n < 360; n++)
				{
					Point p = new Point(center.x + radius * Math.sin(n), center.y + radius * Math.cos(n));
					// 附近的点是否存在颜色
					boolean breakTag = false;
					for (int x = -1; x < 1; x++)
					{
						for (int y = -1; y < 1; y++)
						{
							int xn = (int) (Math.floor(p.x) + x);
							int yn = (int) (Math.floor(p.y) + y);
							if (xn >= 0 && xn < gary.width() && yn >= 0 && yn < gary.height())
							{
//								System.err.println(xn + "--" + gary.width() + "--" + yn + "--" + gary.height());
								int pixel = sourceBufferIamge.getRGB(xn, yn);
								int[] rgb = new int[3];
								rgb[0] = (pixel & 0xff0000) >> 16;
								rgb[1] = (pixel & 0xff00) >> 8;
								rgb[2] = pixel & 0xff;
								if (!(rgb[0] == 255 && rgb[1] == 255 && rgb[2] == 255))
								{
									Imgproc.circle(src, p, 1, new Scalar(0, 255, 0), 2);
									Imgcodecs.imwrite("D:\\pictest\\c.jpg", src);
									connectPoint++;
									breakTag = true;
									break;
								}
							}
							if (breakTag)
							{
								break;
							}
						}
					}
					if (connectPoint >= 90)
					{
						Imgcodecs.imwrite("D:\\pictest\\c.jpg", src);
						return true;
					}
				}
			}
		}
		Imgcodecs.imwrite("D:\\pictest\\c.jpg", src);
		return false;
	}

	/**
	 * OpenCV-4.6.0 霍夫变换-圆形检测
	 *
	 * @return: void
	 * @date: 2019年7月10日 上午12:26:07
	 */
	public static boolean houghCircles(String targetImagePath, int[] wh)
	{
		File outFile = new File(targetImagePath);
		if (!outFile.exists())
		{
			return false;
		}
		Mat src = Imgcodecs.imread(targetImagePath);
		Mat dst = src.clone();

//		 IMREAD_UNCHANGED = -1 ：不进行转化，比如保存为了16位的图片，读取出来仍然为16位。
//		 IMREAD_GRAYSCALE = 0 ：进行转化为灰度图，比如保存为了16位的图片，读取出来为8位，类型为CV_8UC1。
//		 IMREAD_COLOR = 1 ：进行转化为三通道图像。
//		 IMREAD_ANYDEPTH = 2 ：如果图像深度为16位则读出为16位，32位则读出为32位，其余的转化为8位。
//		 IMREAD_ANYCOLOR = 4 ：图像以任何可能的颜色格式读取
//		 IMREAD_LOAD_GDAL = 8 ：使用GDAL驱动读取文件，GDAL(Geospatial Data Abstraction
//		 Library)是一个在X/MIT许可协议下的开源栅格空间数据转换库。它利用抽象数据模型来表达所支持的各种文件格式。
//		  它还有一系列命令行工具来进行数据转换和处理。

		// 锐化 https://blog.csdn.net/ren365880/article/details/103933835
		Mat kernel = new Mat(3, 3, Imgcodecs.IMREAD_COLOR);
		kernel.put(0, 0, 0, -1, 0, -1, 5, -1, 0, -1, 0);
		Imgproc.filter2D(dst, dst, -1, kernel);
		// 高斯模糊
		Imgproc.GaussianBlur(dst, dst, new Size(3, 3), 3);
		// 中值滤波
		Imgproc.medianBlur(dst, dst, 7);

		Mat gary = new Mat();
		Imgproc.cvtColor(dst, gary, Imgproc.COLOR_BGR2GRAY);

		Mat circles = new Mat();
//		
//		 该函数使用霍夫变换来找到灰度图像中的圆。
//		 注意：通常该功能可以很好地检测圆心。但是，它可能找不到正确的半径。如果知道半径范围（minRadius和maxRadius），则可以辅助该功能。或者，
//		 您可以将maxRadius设置为负数以仅返回中心而不进行半径搜索，并使用其他过程查找正确的半径。
//		 @param图像8位单通道灰度输入图像。
//		 @param circle输出找到的圆的向量。每个向量都被编码为3或4个浮点向量（（x，y，radius））或（（x，y，radius，votes））。
//		 @param方法检测方法，请参见#HoughModes。当前，唯一实现的方法是#HOUGH_GRADIENT
//		 @param dp累加器分辨率与图像分辨率的反比。例如，如果dp = 1，则累加器具有与输入图像相同的分辨率。如果dp = 2，则累加器的宽度和高度是其一半。
//		 @param minDist检测到的圆心之间的最小距离。如果参数太小，则除了真实的圆圈外，还可能会错误地检测到多个邻居圆圈。如果太大，可能会错过一些圆。
//		 @param param1第一个方法特定的参数。在#HOUGH_GRADIENT的情况下，它是传递给Canny边缘检测器的两个阈值中的较高阈值（较低的那个要小两倍）
//		 @param param2第二个方法特定的参数。在#HOUGH_GRADIENT的情况下，它是检测阶段圆心的累加器阈值。它越小，可能会检测到更多的假圆圈。
//		 与较大的累加器值相对应的圆将首先返回。
//		 @param minRadius最小圆半径。
//		 @param maxRadius最大圆半径。如果<= 0，则使用最大图像尺寸。如果<0，则返回中心而未找到半径。
//		 
		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1.5, 200, 130, 25, 10, 500);
//		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1, 50, 400, 20, 5, 1000);
//		Imgproc.HoughCircles(gary, circles, Imgproc.HOUGH_GRADIENT, 1, 300, 400, 100, 2, 1000);
		for (int i = 0; i < circles.rows(); i++)
		{
			double[] vCircle = circles.get(i, 0);

			Point center = new Point(vCircle[0], vCircle[1]);
			int radius = (int) Math.round(vCircle[2]);

//				
//				 画一个圆。 函数cv :: circle绘制具有给定中心和半径的简单或实心圆。
//				 @param img绘制圆的图像。
//				 @param center圆心。
//				 @param 圆的半径。
//				 @param color圆形颜色。
//				 @param 圆轮廓的宽度（如果为正）。 负值表示要绘制一个实心圆。
//				 @param lineType圆边界的类型。 请参阅https://blog.csdn.net/ren365880/article/details/103952856
//				
			// circle center
//			Imgproc.circle(src, center, 1, new Scalar(0, 255, 0), 2);
			// circle outline
			Imgproc.circle(src, center, radius, new Scalar(0, 0, 255), 2);
		}
		HighGui.imshow("霍夫变换-圆形检测", src);
		HighGui.waitKey(1);
//		Imgcodecs.imwrite("D:\\c.jpg", src);
		return circles.rows() > 0;

	}

	/**
	 * （颜色像素）提取
	 * 
	 * @param sourceImagePath
	 * @param targetImagePath
	 * @param targetImageWidth
	 * @param targetImageHeight
	 * @return
	 */
	public static int[] rgbRed(String sourceImagePath, String targetImagePath)
	{
		int[] rgb = new int[3];

		Graphics2D graphics2D = null;
		try
		{
			File outFile = new File(targetImagePath);
			FileUtils.deleteQuietly(outFile);
			File imageFile = new File(sourceImagePath);
			BufferedImage sourceBufferIamge = ImageIO.read(imageFile);

			int width = sourceBufferIamge.getWidth();
			int height = sourceBufferIamge.getHeight();
			int imageMinX = sourceBufferIamge.getMinX();
			int iamgeMinY = sourceBufferIamge.getMinY();

			int maxx = 0;
			int maxy = 0;
			int minx = 0;
			int miny = 0;

			// 循环获取最大最小xy坐标
			for (int i = imageMinX; i < width; i++)
			{
				for (int j = iamgeMinY; j < height; j++)
				{
					int pixel = sourceBufferIamge.getRGB(i, j);
					rgb[0] = (pixel & 0xff0000) >> 16;
					rgb[1] = (pixel & 0xff00) >> 8;
					rgb[2] = pixel & 0xff;
					double d = Math.pow((256 - rgb[0]), 2) + Math.pow(rgb[1], 2) + Math.pow(rgb[2], 2);
					boolean isRed = Math.sqrt(d) < distance;
					if (isRed)
					{
						if (minx == 0 || minx > i)
						{
							minx = i;
						}
						if (miny == 0 || miny > j)
						{
							miny = j;
						}
						if (maxx < i)
						{
							maxx = i;
						}
						if (maxy < j)
						{
							maxy = j;
						}
					}
				}
			}

			// 计算剪裁图片大小
			// 图片宽度
			int imageW = maxx - minx;
			// 图片高度
			int imageH = maxy - miny;

			if (imageW > 0 && imageH > 0)
			{

				// 剪裁图片
				BufferedImage subImage = sourceBufferIamge.getSubimage(minx, miny, imageW, imageH);

				// 缩放图片
				Image scaledImage = subImage.getScaledInstance(sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), Image.SCALE_SMOOTH);
				BufferedImage targetBufferImage = new BufferedImage(sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
				graphics2D = targetBufferImage.createGraphics();
				graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
				graphics2D.drawImage(scaledImage, 0, 0, sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), null);

				// 替换底色
				int targetW = targetBufferImage.getWidth();
				int targetH = targetBufferImage.getHeight();
				int targetMinX = targetBufferImage.getMinX();
				int targetMinY = targetBufferImage.getMinY();

				for (int i = targetMinX; i < targetW; i++)
				{
					for (int j = targetMinY; j < targetH; j++)
					{
						// 得到指定像素（i,j)上的RGB值
						int pixel = targetBufferImage.getRGB(i, j);
						// 分别进行位操作得到 r g b上的值
						rgb[0] = (pixel & 0xff0000) >> 16;
						rgb[1] = (pixel & 0xff00) >> 8;
						rgb[2] = pixel & 0xff;
						double d = Math.pow((256 - rgb[0]), 2) + Math.pow(rgb[1], 2) + Math.pow(rgb[2], 2);
						boolean isRed = Math.sqrt(d) < distance;
						if (!isRed)
						{
							// 这里是判断通过，则把该像素换成透明
							// bi.setRGB(i, j, 0x000000);
							targetBufferImage.setRGB(i, j, 0x00ffffff);
						}
					}
				}

				// 将缓冲对象保存到新文件中
				FileOutputStream ops = new FileOutputStream(outFile);
				ImageIO.write(targetBufferImage, "png", ops);
				ops.flush();
				ops.close();
				return new int[] { sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight() };
			}
			else
			{
				System.err.println("未提取到印章");
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally
		{
			if (graphics2D != null)
			{
				graphics2D.dispose();
			}
		}
		return new int[] { 0, 0 };
	}

	/**
	 * （颜色像素）提取
	 * 
	 * @param sourceImagePath
	 * @param targetImagePath
	 * @param targetImageWidth
	 * @param targetImageHeight
	 * @return
	 */
	public static int[] signatureProcess(String sourceImagePath, String targetImagePath)
	{
		int[] rgb = new int[3];

		Graphics2D graphics2D = null;
		try
		{
			File outFile = new File(targetImagePath);
			FileUtils.deleteQuietly(outFile);
			File imageFile = new File(sourceImagePath);
			BufferedImage sourceBufferIamge = ImageIO.read(imageFile);

			int width = sourceBufferIamge.getWidth();
			int height = sourceBufferIamge.getHeight();
			int imageMinX = sourceBufferIamge.getMinX();
			int iamgeMinY = sourceBufferIamge.getMinY();

			int maxx = 0;
			int maxy = 0;
			int minx = 0;
			int miny = 0;

			// 循环获取最大最小xy坐标
			for (int i = imageMinX; i < width; i++)
			{
				for (int j = iamgeMinY; j < height; j++)
				{
					int pixel = sourceBufferIamge.getRGB(i, j);
					rgb[0] = (pixel & 0xff0000) >> 16;
					rgb[1] = (pixel & 0xff00) >> 8;
					rgb[2] = pixel & 0xff;
					float[] hsv = Color.RGBtoHSB(rgb[0], rgb[1], rgb[2], null);
					// 纯红印章 RGB(255,0,0)
					hsv[0] = hsv[0] * 360;
					hsv[1] = hsv[1] * 100;
					hsv[2] = hsv[2] * 100;
					if ((hsv[0] >= 330 && hsv[0] <= 360 || hsv[0] >= 0 && hsv[0] <= 10 || hsv[0] >= 156 && hsv[0] <= 180) && (hsv[1] >= 43 && hsv[1] <= 255) && (hsv[2] >= 46 && hsv[2] <= 255))
					{
						if (minx == 0 || minx > i)
						{
							minx = i;
						}
						if (miny == 0 || miny > j)
						{
							miny = j;
						}
						if (maxx < i)
						{
							maxx = i;
						}
						if (maxy < j)
						{
							maxy = j;
						}
					}
				}
			}

			// 计算剪裁图片大小
			// 图片宽度
			int imageW = maxx - minx;
			// 图片高度
			int imageH = maxy - miny;

			if (imageW > 0 && imageH > 0)
			{

				// 剪裁图片
				BufferedImage subImage = sourceBufferIamge.getSubimage(minx, miny, imageW, imageH);

				// 缩放图片
				Image scaledImage = subImage.getScaledInstance(sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), Image.SCALE_SMOOTH);
				BufferedImage targetBufferImage = new BufferedImage(sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
				graphics2D = targetBufferImage.createGraphics();
				graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
				graphics2D.drawImage(scaledImage, 0, 0, sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight(), null);

				// 替换底色
				int targetW = targetBufferImage.getWidth();
				int targetH = targetBufferImage.getHeight();
				int targetMinX = targetBufferImage.getMinX();
				int targetMinY = targetBufferImage.getMinY();

				for (int i = targetMinX; i < targetW; i++)
				{
					for (int j = targetMinY; j < targetH; j++)
					{
						// 得到指定像素（i,j)上的RGB值
						int pixel = targetBufferImage.getRGB(i, j);
						// 分别进行位操作得到 r g b上的值
						rgb[0] = (pixel & 0xff0000) >> 16;
						rgb[1] = (pixel & 0xff00) >> 8;
						rgb[2] = pixel & 0xff;

						if (rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0)
						{
							// 这里是判断通过，则把该像素换成透明
							// bi.setRGB(i, j, 0x000000);
							targetBufferImage.setRGB(i, j, 0x00ffffff);
						}
					}
				}

				// 将缓冲对象保存到新文件中
				FileOutputStream ops = new FileOutputStream(outFile);
				ImageIO.write(targetBufferImage, "png", ops);
				ops.flush();
				ops.close();
				return new int[] { sourceBufferIamge.getWidth(), sourceBufferIamge.getHeight() };
			}
			else
			{
				System.err.println("未提取到印章");
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally
		{
			if (graphics2D != null)
			{
				graphics2D.dispose();
			}
		}
		return new int[] { 0, 0 };
	}

}
